React Native WebView 오프라인 런타임 설계
React Native WebView 오프라인 런타임 설계
오프라인 WebView는 HTML 캐시 하나로 완성되지 않는다. Native와 Web의 책임 경계, 버전이 있는 브릿지, 원자적인 자산 교체, 멱등적인 작업 큐를 함께 설계해야 한다.
본문의 코드는 특정 저장소 구현을 복사하지 않고 개념을 설명하기 위해 재구성한 예시다. 이름·경로·수치는 실제 운영 정보와 무관하다.
WebView 앱을 오프라인에서도 동작하게 만들려고 하면 먼저 “페이지를 캐시하면 되지 않을까?”라고 생각하게 된다. 하지만 실제 문제는 HTML 파일 하나를 여는 것보다 훨씬 넓다.
- 어떤 자산을 언제 받을 것인가
- 다운로드 중 종료되면 어떻게 복구할 것인가
- 오프라인 중의 사용자 작업을 어디에 저장할 것인가
- 네트워크가 돌아오면 어떤 순서로 동기화할 것인가
- Web이 Native의 상태를 어떻게 알 것인가
결국 오프라인 WebView는 캐시 기능이 아니라 작은 런타임을 설계하는 문제에 가깝다.
목차
- #Native와 Web의 책임을 나눈다
- #브릿지는 문자열 통로가 아니라 프로토콜이다
- #자산 교체는 원자적으로
- #오프라인 큐에는 idempotency가 필요하다
- #정책은 코드 여러 곳에 퍼뜨리지 않는다
- #결론
- #관련 노트
Native와 Web의 책임을 나눈다
처음에 해야 할 것은 책임 경계를 정하는 일이다.
Native 런타임이 담당할 일:
- 네트워크 상태 감지
- 앱 버전에 맞는 웹 번들 다운로드
- 파일 무결성 검사와 원자적 교체
- 오프라인 작업 큐 저장
- 재시도와 동기화 스케줄링
Web 런타임이 담당할 일:
- 화면과 도메인 상태
- 요청을 즉시 보낼지 큐에 넣을지 판단
- 사용자에게 동기화 상태 표시
- Native 브릿지와 요청·응답 메시지 교환
이 경계가 없으면 Web은 앱 파일 시스템을 직접 알아야 하고, Native는 개별 비즈니스 API를 모두 알아야 한다. 두 층이 서로의 세부 구현에 묶이는 구조다.
브릿지는 문자열 통로가 아니라 프로토콜이다
WebView의 postMessage 예제는 단순해 보이지만, 실제 앱에서는 메시지 규약이 필요하다.
type BridgeMessage = {
id: string;
type: "QUEUE_COMMAND" | "SYNC_NOW" | "NETWORK_CHANGED";
version: 1;
payload: unknown;
};
최소한 다음 정보는 있어야 한다.
id: 요청과 응답 매칭, 중복 처리 방지type: 메시지 의미version: 앱과 웹 버전이 엇갈릴 때의 호환성payload: 타입별 데이터
수신측은 알 수 없는 type과 버전을 무시하거나 명시적으로 거절해야 한다. 문자열을 바로 실행 명령으로 해석하는 구조는 피해야 한다.
자산 교체는 원자적으로
새 번들을 기존 폴더 위에 바로 덮어쓰면 다운로드가 끊긴 순간 불완전한 상태가 된다. 더 안전한 순서는 다음과 같다.
- 임시 디렉터리에 다운로드한다.
- manifest의 파일 목록과 hash를 검증한다.
- 기존 active 버전을 rollback 용으로 남겨 둔다.
- active 포인터를 새 버전으로 교체한다.
- 앱 시작이 성공하면 이전 버전을 정리한다.
새 버전이 깨져 있어도 직전의 정상 버전으로 돌아갈 수 있어야 한다.
오프라인 큐에는 idempotency가 필요하다
사용자가 오프라인에서 등록 버튼을 눌렀다면 작업을 큐에 넣어야 한다. 하지만 네트워크는 “요청이 서버에 도착했지만 응답만 끊긴” 상태를 만든다. 단순 재시도는 중복 등록을 만들 수 있다.
각 command에 고유한 idempotency key를 부여하고 서버가 같은 key의 재실행을 같은 결과로 처리해야 한다. 큐는 최소한 다음 상태를 가진다.
pending -> running -> succeeded
-> retry_wait
-> failed
실패 횟수만 저장하기보다 다음 재시도 시각과 마지막 오류를 함께 남겨야 운영할 수 있다.
정책은 코드 여러 곳에 퍼뜨리지 않는다
요청마다 if (!online)을 반복하면 정책이 화면 코드 전체에 퍼진다. 요청의 성격을 선언하고 런타임이 처리 방식을 결정하게 만드는 편이 낫다.
const policy = {
method: "POST",
offline: "queue",
retry: { maxAttempts: 5 },
requiresAuth: true,
};
오프라인 기능은 예외 처리가 아니라 실행 정책이 된다.
결론
오프라인 WebView의 핵심은 다음 네 가지였다.
- Native와 Web의 책임 경계
- 버전을 가진 브릿지 프로토콜
- 검증과 rollback이 가능한 자산 캐시
- idempotency를 가진 오프라인 작업 큐
페이지를 저장하는 것만으로는 오프라인 앱이 되지 않는다. 실패한 네트워크와 다른 버전이 공존하는 상태를 정상적인 제품 상태로 다룰 수 있어야 한다.
오프라인은 예외 상태가 아니라 언제든 진입할 수 있는 정상 상태다. 복구와 버전 불일치를 처음부터 프로토콜의 일부로 다뤄야 한다.
관련 노트
- offline webview — 오프라인 WebView 구현 아이디어와 참고 사항
- ios flutter webview에서 터치 이벤트 호출되는 문제 — iOS WebView 이벤트 문제 기록
- flutter 웹뷰 뒤로가기 — WebView 내비게이션 처리 기록